Skip to content

test: refuse to measure a binary that was not built from this source - #898

Merged
jdatcmd merged 7 commits into
commandprompt:mainfrom
OffgridwithJD:audit/build-freshness-controller
Sep 9, 2026
Merged

test: refuse to measure a binary that was not built from this source#898
jdatcmd merged 7 commits into
commandprompt:mainfrom
OffgridwithJD:audit/build-freshness-controller

Conversation

@OffgridwithJD

Copy link
Copy Markdown
Collaborator

The matrix builds once per major and then runs every suite with PGC_SKIP_BUILD=1. Nothing checked that the binary those suites measured came from the tree under test. This adds that check, at the controller, so a stale .so cannot be measured and nothing has to rebuild per test.

Two failure modes, both of which have bitten this project before, and both now FATAL rather than advisory.

1. The binary was not built from this source

The controller records a fingerprint of the build inputs after a successful install. Every suite compares it, whether that suite built or skipped.

-- source: 28b66bd0ac0c matches the binary under test

Proved by removal, with controls on both sides:

arm result
clean skip-build run exit 0, fingerprint matches
source changed, no rebuild exit 1
restored, re-run exit 0, same fingerprint as the first arm

The red arm reports:

FATAL: the binary under test was not built from this source
       source now 25f824613a73, binary built from 28b66bd0ac0c
       (refusing to report checks about code that is not installed)

It names both fingerprints, because "stale" without the two values leaves the reader unable to tell a real drift from a broken fingerprint.

2. The server predates the binary

A make install does not reload anything. shared_preload_libraries maps the library at postmaster start, so a reinstall under a running server leaves the backends executing the old code while the file on disk is new. The check compares the .so mtime against pg_postmaster_start_time():

-- server: started after the binary was installed

and on the bad verdict says what to do rather than only what is wrong:

FATAL: this server was already running when the binary changed
       shared_preload_libraries maps the library at start, so the
       backends are executing older code than the file on disk.
       Restart the cluster; a reinstall alone does not reload it.

unknown is not a failure, deliberately

Someone who ran make install by hand has no stamp. Refusing would break a documented workflow, so that case prints

-- source: <fingerprint>, freshness UNVERIFIED (no stamp for major 18)

The point is that it says which question was not answered, rather than printing nothing and letting a reader assume the check passed. A silent third state is how a verdict becomes lossy.

The verdict functions are pure, and tested as such

pgc_freshness_verdict and pgc_running_binary_verdict take strings and return fresh / stale / unknown and fresh / predates / unknown. They touch no filesystem, so test/selftest/340-the-binary-must-be-built-from.sh tests them directly — 16 arms including both empty inputs, a non-numeric epoch, equal timestamps on the exact boundary, and fingerprint sensitivity to each input class.

harness_selftest goes from 261 checks to 277.

Two defects this found in itself

The stamp was written in the wrong branch first. My initial version wrote it in the skip-build path, which made the check tautological: it recomputed the fingerprint of the source it had just read and reported "matches" on edited source. My own red arm caught it. The stamp is now written only after a successful install; the verify runs always.

The controller swallowed its own failure. The stamp write was ... || true. If it failed, every suite in the batch would report freshness UNVERIFIED and the controller arm would silently stop being a controller arm — the exact state this exists to prevent, with nothing saying so. It now prints what a failure means for the run below it.

Verification

Full matrix on both majors at 0332527:

PG18   244 of 246 suites ran (2 skipped, 0 incomplete)   zero FAIL
PG19   246 of 246 suites ran (0 skipped, 0 incomplete)   zero FAIL
ALL VERSIONS PASSED

A full matrix is the right bar here and there was no shortcut available: this changes test/lib.sh, which every suite reads.

The check was observed working inside that matrix, not only in isolation. Sampling the live PG19 per-suite logs three times during the run:

47 logs   42 report fresh    0 UNVERIFIED   0 FATAL
84 logs   79 report fresh    0 UNVERIFIED   0 FATAL
173 logs  168 report fresh   0 UNVERIFIED   0 FATAL

The five that report nothing are accounted for rather than assumed: audit, bench_guards, concurrency and docs_style never call pgc_setup, and decode_interrupts is a static source-analysis suite whose only occurrence of pgc_setup is the comment "No cluster needed; pgc_setup is skipped deliberately." — my first grep -c counted that comment as a call.

.gitignore gains .pgc_source_stamp.* beside .pgc_built_for_major, which is the same kind of file for the same reason. Verified the new rule is the one matching — the file was not ignored before, and git check-ignore -v now names .gitignore:10 — rather than assuming my rule was what caught it.

@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

OffgridwithJD and others added 4 commits September 9, 2026 15:18
…r runs it

jd's design: one controller arm per batch of tests, so a stale .so can never be
measured and nothing has to rebuild per test.

TWO QUESTIONS THE HARNESS COULD NOT ANSWER.

selftest 110 compares the INSTALLED .so against the one built in this tree, so a
missed install and a foreign overwrite were already caught. Nothing derived anything
from the SOURCE TEXT, so both copies could agree with each other while both were
stale against edited source. PGC_SKIP_BUILD opens that hole widest, because not
rebuilding is its whole purpose.

And a cp is not enough. shared_preload_libraries maps the library at postmaster
start, so make install over a running instance changes the file and nothing else:
every backend keeps executing the code it already mapped. A binary can match the
source exactly while the server runs something older.

THE SHAPE. Whoever builds records a fingerprint of the build inputs -- src/*.c,
src/*.h, the Makefile, the control file, the shipped SQL. Every suite in the batch
recomputes it and compares, which is one build per batch and one hash per suite. Then
once the cluster is up, the suite compares the binary's mtime against
pg_postmaster_start_time().

    -- source: 28b66bd0ac0c matches the binary under test
    -- server: started after the binary was installed

A stale source fingerprint and a server predating the binary are both FATAL, because
every check that followed would be about code that is not running. A missing stamp is
UNVERIFIED and said plainly rather than failed: a person who ran make install by hand
has no stamp, and refusing would break a documented workflow.

MEASURED, three arms:

    normal build, install, run          source matches, server fresh, PASSED
    source edited + PGC_SKIP_BUILD=1    FATAL: not built from this source, exit 1
    binary newer than the postmaster    FATAL: server already running, exit 1

The two verdict functions are pure and take their inputs as arguments, for the same
reason pgc_build_needs_clean does, so selftest 340 exercises fresh, stale, unknown,
predates and the non-numeric cases without a build. It also requires that the
fingerprint MOVES when a build input moves, STAYS when nothing does, and ignores a
file that is not a build input -- a fingerprint that never changes reports fresh
forever, which is this file's own failure mode one level down.

WHAT WROTE THE STAMP IN THE WRONG PLACE, AND WHAT CAUGHT IT. The first revision wrote
it in the skip-build branch, so every run recorded the source it was about to compare
against and a suite measuring an edited tree reported "matches the binary under test".
The arm that requires `stale` is what caught it. The stamp is now written only where
the install succeeded, and the comment there says why.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
…lt from

jd's ask: a controller arm that verifies the build is fresh against the batch of
tests being run, so we never measure a stale .so but also do not rebuild for
every test.

run_all_versions.sh is that controller. It builds and installs once per major and
then runs every suite with PGC_SKIP_BUILD=1, so the suites have no way of their
own to tell whether the binary they measure came from this tree. It now records
the fingerprint of the build inputs after a successful install, and lib.sh checks
it in every suite whether that suite built or skipped.

In a subshell sourcing lib.sh rather than recomputing the hash inline: two
implementations of one fingerprint drift, and the suites compare against exactly
what this writes.

Not `|| true`. If the stamp cannot be written, every suite in the batch reports
"freshness UNVERIFIED" and the controller silently stops being a controller --
the batch degrades to the state this exists to prevent, with nothing saying so.
A failure now prints what it means for the run below it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
pgc_setup writes .pgc_source_stamp.<major> beside the source it fingerprints, so
running any suite from a checkout left an untracked file in `git status`. This
project gates on a clean tree, so a harness artifact that dirties one is a
recurring false alarm rather than a cosmetic issue.

Placed next to .pgc_built_for_major, which is the same kind of file written for
the same reason. Verified the new rule is the one that matches -- the file was
NOT ignored before, and `git check-ignore -v` now names .gitignore:10 -- and that
the existing rule still matches its own file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
Same omission as commandprompt#897: this project records test-infrastructure changes in
CHANGELOG.md and I opened the PR without an entry.

The entry says what both checks refuse, and says that neither fails when it
cannot answer -- an unstamped tree prints "freshness UNVERIFIED" and names the
question it did not answer, rather than printing nothing and reading as a pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

Note for whoever merges second: #897 and #898 both edit the same block of test/lib.sh, and the interaction is semantic, not just textual.

#898 writes the source stamp inside pgc_setup's build branch:

lib.sh:233   pgc_write_source_stamp \
                 "$(pgc_source_stamp_path "$PGC_SRCDIR" "$PGC_MAJOR")" \
                 "$(pgc_source_fingerprint "$PGC_SRCDIR")"

#897 extracts that same branch into pgc_build_and_install, so the pytest harness can drive one implementation instead of carrying a second.

Git will merge these without a conflict in at least one order, and the result would be wrong in a way no test would catch: the stamp write must stay inside the extracted function, after the install that succeeded. If it ends up outside, it either stops being written for bash suites, or gets written on a path that did not build — which is precisely the tautology #898's own comment records catching once already.

Whichever lands first, the second should be rebased with the stamp write placed inside pgc_build_and_install, between the successful make install and the return 0. I will do that rebase rather than leave it to the merge, and will re-gate rather than assume the move is inert, since test/lib.sh is read by every suite.

No action needed on this PR right now; recording it so it is not discovered at merge time.

@jdatcmd jdatcmd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at c6e20581. The mechanism works and the matrix path is genuinely protected — I
reproduced your red arm exactly. Three gaps, all in what the fingerprint does not see or where the
check does not run. Requesting changes because the failure mode of each is a false assurance
rather than an absent one, and this PR exists to remove exactly that.

1. The fingerprint does not cover objstore/, and that module is separately built and installed

pgc_source_fingerprint reads $dir/src/*.{c,h} at maxdepth 1 plus the top-level Makefile,
*.control and *.sql. objstore/columnar_objstore_module.c is a build input — Makefile:132-143
runs $(MAKE) -C $(OBJSTORE_DIR) for all, install and clean, producing and installing
pgcolumnar_objstore.so.

Demonstrated, with a control so this is the gap and not a broken probe:

baseline                                  -- source: 28b66bd0ac0c matches the binary under test
edit objstore/columnar_objstore_module.c  -- source: 28b66bd0ac0c matches the binary under test   <-- stale
edit src/columnar_projection.c (control)  FATAL: the binary under test was not built from this source
                                                 source now 8e07a5495a31, binary built from 28b66bd0ac0c
restored                                  -- source: 28b66bd0ac0c matches the binary under test

So objstore_module, objstore_sink_write and objstore_stash_recovery can measure a stale
module while the suite prints an explicit assurance that the binary matches. That is worse than
printing nothing, because the line is what stops the next person checking.

The fix is one find line. Whichever way you extend it, the property worth asserting in the
selftest is "every directory the Makefile builds from is in the fingerprint" rather than a list
that has to be remembered — objstore/ was added once and will not be the last.

2. The postmaster arm cannot fire through any shipped path

pgc_setup is unconditional:

PGC_WORKDIR="$(mktemp -d /tmp/pgcolumnar-test.XXXXXX)"
PGC_PGDATA="$PGC_WORKDIR/data"
... initdb -D "$PGC_PGDATA" ... pg_ctl -D "$PGC_PGDATA" ... start

Neither variable is overridable and nothing in test/ supplies a datadir, so every suite
initdbs a fresh cluster and starts it after the .so was installed
. _pm_epoch is therefore
always greater than _so_epoch, the verdict is always fresh, and predates is unreachable.

test/selftest/340 tests pgc_running_binary_verdict as a pure function with fixture values
(2000, 1000 -> predates). That proves the arithmetic and not the call site — this project's own
rule: a suite that sources a helper cannot see whether anything calls it; feeding a function
fixtures proves its arithmetic and nothing else, so the caller can be deleted with every check
still passing.

I am not saying delete it. The hazard is real and the message is the best one in the PR. But as it
stands the tree carries a check that cannot report the condition it names, and the selftest reads
as though it does. Either name a path where a cluster outlives a reinstall and give it an arm, or
say in the comment that this arm guards a workflow the suites themselves never take.

3. devloop.sh — the documented dev loop — gets UNVERIFIED, not enforced

This is the one I would fix first, because it is the loop a human uses while editing C.

$ devloop.sh ... drop_cleanup
-- source: 28b66bd0ac0c, freshness UNVERIFIED (no stamp for major 18)

devloop.sh:92 runs the suites with PGC_SKIP_BUILD=1 after building itself, and only
run_all_versions.sh:725 writes the stamp. So the whole class this PR exists to catch — edit a
file, forget to rebuild, measure the old binary — is unprotected in precisely the loop where it
happens, and the unknown branch prints and continues by design.

devloop.sh already builds and installs, so it can write the stamp in the same place, and it is
the natural owner: it is the other thing in the tree that installs before running suites.

What is right

The red arm reproduces exactly, and naming both fingerprints in the message is the detail that
makes it debuggable rather than merely loud — source now X, binary built from Y tells a reader
whether they are looking at drift or at a broken fingerprint, which "stale" alone does not.

unknown not being a failure is the correct call for the hand-make install workflow, and the
selftest covers all four verdicts in both directions including the non-numeric case.

And your two disclosed self-findings are the kind that matter: the stamp write being || true
would have left every suite reporting freshness UNVERIFIED silently — the exact state the arm
exists to prevent — and the untracked stamp tripping the clean-tree rule. Verifying that the NEW
.gitignore rule is the one matching, rather than assuming, is the right instinct; it is the same
shape as a mutation you have to prove applied.

One note on scope

test/lib.sh is sourced by every suite, so I agree with running the full matrix on both majors
rather than a reduced set — there is no equivalent here of the CHANGELOG.md-only argument that
justified the reduced re-gate on #892.

All three from @jdatcmd's review. Each failed the same way: the check reported
an assurance it had not earned, which is worse than reporting nothing, because
the line is what stops the next person looking.

THE FINGERPRINT DID NOT SEE objstore/. It read $dir/src only, and
objstore/columnar_objstore_module.c is a build input -- the top-level Makefile
builds and installs it by recursion as a separate shared library. Editing it
left the fingerprint unchanged, so objstore_module, objstore_sink_write and
objstore_stash_recovery could measure a stale module while the run printed
"matches the binary under test". Reproduced with his control:

    baseline                                  1449dc9dba17
    edit objstore/columnar_objstore_module.c  c85763de5bb5  CHANGED
    edit src/columnar_projection.c (control)  4fffcff2b65f  CHANGED
    edit objstore/Makefile                    75d57e06199d  CHANGED
    restored                                  1449dc9dba17  back to baseline

Naming objstore/ would fix today and fail the next time a module is added, so
pgc_source_build_dirs DERIVES the set: every directory with its own Makefile,
the same rule the build follows. The selftest asserts the property rather than
the list -- it parses the `$(MAKE) -C` recursion out of the Makefile and
requires every target to be covered -- with a premise check that the parse found
something, because a guard that found nothing to check has abstained rather than
passed. Proved by removal: reverting to src-only reddens both arms with
`missing: objstore`.

THE POSTMASTER ARM COULD NOT FIRE THROUGH ANY SHIPPED PATH. pgc_setup always
initdb's a fresh cluster and starts it after the install, so the postmaster is
always newer than the .so and `predates` was unreachable. selftest 340 fed the
verdict function fixture values, which proves its arithmetic and not its call
site: the call could have been deleted with every check still passing.

pgc_check_running_binary is now extracted from pgc_setup and takes the library
path as an argument. The selftest points it at a file it has just touched, so
the stat, the pg_postmaster_start_time() query, the verdict and the refusal all
run for real against the live cluster and only the path is redirected -- nothing
has to touch the installed library to prove the guard fires. Proved by removal:
neutering the `return 1` reddens "a library newer than the running server is
REFUSED" with got [0] want [1].

devloop.sh GOT UNVERIFIED, NOT ENFORCED, and it is the loop a human uses while
editing C. It builds and installs and then runs suites with PGC_SKIP_BUILD=1,
and only run_all_versions.sh wrote the stamp -- so edit-a-file-and-forget-to-
rebuild was unprotected in exactly the place it happens. devloop is a controller
and now records what it installed. Measured end to end:

    A  devloop, clean tree   -- source: 1449dc9dba17 matches the binary under test
    B  edit C, no rebuild    exit 1, FATAL, source now c636277e827f,
                             binary built from 1449dc9dba17
    C  restore, same command exit 0, matches, PASSED

pgc_major_of is extracted alongside it because devloop writes a stamp whose PATH
is keyed on the major and pgc_setup reads it back. Two copies of that sed would
be two answers to "which major", and the failure would be a stamp written where
nothing looks for it: a silent UNVERIFIED rather than an error.

harness_selftest goes from 277 checks to 288.

One process note. My first removal proof for the objstore fix SILENTLY FAILED TO
APPLY -- nested-heredoc escaping -- and printed PASS. The assert inside the
mutation caught it. Without that I would have reported a guard as proven when it
had never been exercised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
@OffgridwithJD
OffgridwithJD force-pushed the audit/build-freshness-controller branch from c6e2058 to 9058515 Compare September 9, 2026 16:17
@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

Reworked at 9058515, rebased onto f2af0809. All three closed, each proved by removal rather than by inspection.

1. The fingerprint now derives its build directories

Naming objstore/ would have fixed today and failed the next time a module is added, so pgc_source_build_dirs returns every directory with its own Makefile — the same rule the build follows. Your demonstration, reproduced with your control:

baseline                                  1449dc9dba17
edit objstore/columnar_objstore_module.c  c85763de5bb5  CHANGED
edit src/columnar_projection.c (control)  4fffcff2b65f  CHANGED
edit objstore/Makefile                    75d57e06199d  CHANGED
restored                                  1449dc9dba17  back to baseline

The selftest asserts the property you asked for rather than a list: it parses the $(MAKE) -C $(X_DIR) recursion out of the top-level Makefile and requires every target to be covered. It also carries a premise check that the parse found something, because a guard that found nothing to check has abstained rather than passed.

Proved by removal — reverting pgc_source_build_dirs to src-only:

FAIL  every directory the Makefile builds from is in the fingerprint: got [missing: objstore] want [none]
FAIL  a new source file under objstore moves the fingerprint: got [same] want [moved]

2. The postmaster arm can now fire, and is driven

You were right that feeding the verdict fixture values proves its arithmetic and not its call site — that is this project's own rule and I had shipped exactly what it warns about.

pgc_check_running_binary is extracted from pgc_setup and takes the library path as an argument. The selftest points it at a file it has just touched, so the stat, the pg_postmaster_start_time() query, the verdict and the refusal all execute for real against the live cluster; only the path is redirected, so nothing has to touch the installed library to prove the guard fires.

Six arms: refused when newer, the message naming the restart, accepted when older, saying so rather than staying silent, and an unreadable path reading unknown rather than fresh.

Proved by removal — neutering the return 1:

FAIL  a library newer than the running server is REFUSED: got [0] want [1]

That is the arm that could not exist before.

3. devloop.sh writes the stamp

It is a controller — it builds and installs, then runs suites with PGC_SKIP_BUILD=1 — so it now records what it installed. Measured end to end:

A  devloop, clean tree    -- source: 1449dc9dba17 matches the binary under test
                          stamp written: $PGC_BUILD/.pgc_source_stamp.18
B  edit C, no rebuild     exit 1
                          FATAL: the binary under test was not built from this source
                                 source now c636277e827f, binary built from 1449dc9dba17
C  restore, same command  exit 0, matches, PASSED

pgc_major_of is extracted alongside it, and that is load-bearing rather than tidiness: devloop writes a stamp whose path is keyed on the major and pgc_setup reads it back. Two copies of that sed would be two answers to "which major", and the failure would be a stamp written where nothing looks for it — a silent UNVERIFIED rather than an error.

Verification

harness_selftest 277 -> 288 checks. Full matrix on both majors at 9058515:

PG18   247 of 249 suites ran (2 skipped, 0 incomplete)   zero FAIL
PG19   249 of 249 suites ran (0 skipped, 0 incomplete)   zero FAIL
ALL VERSIONS PASSED

Full matrix rather than a reduced set, and I agree with your note on why: test/lib.sh is sourced by every suite, so there is no equivalent of the CHANGELOG.md-only argument that justified the reduced re-gate on #892.

One process note against myself

My first removal proof for the objstore fix silently failed to apply — nested-heredoc escaping — and printed PASS. The assert inside the mutation caught it. Without that I would have reported a guard as proven when it had never been exercised, which is the same defect as the ones this PR removes, one level up.

Still standing

The test/lib.sh interlock with #897 is unchanged: #898 writes the stamp inside pgc_setup's build branch, #897 extracts that branch into pgc_build_and_install. Whichever lands first, I will rebase the second by hand so the stamp write stays inside the extracted function, and re-gate rather than assume the move is inert.

… SC1087)

`$_bd_var[[:space:]]` reads to shellcheck as an array expansion, so
`shellcheck -S error` -- a CI-only job the local gate never runs -- failed
on the selftest arm added in the last push. Braced to `${_bd_var}`.

Semantically identical in bash; proved by running the suite rather than by
inspection. harness_selftest.sh: 288 passed + 0 failed + 0 unrunnable,
including `PREMISE the Makefile's recursion was actually parsed`, which is
the check that goes red if the sed stops resolving the variable name.
`shellcheck -S error -s bash test/*.sh test/selftest/*.sh` now exits 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

CI red on my last push, fixed at b4a77c9. Reporting it because it says something about my gate, not just about the line.

shellcheck (test harness, errors only) failed on the selftest arm I added:

In test/selftest/340-the-binary-must-be-built-from.sh line 110:
	_bd_val="$(sed -n "s/^[[:space:]]*$_bd_var[[:space:]]*=[[:space:]]*\(.*\)$/\1/p" \
                                          ^-- SC1087 (error): Use braces when expanding arrays

$_bd_var[ reads to shellcheck as an array expansion. Braced to ${_bd_var}; that is the whole diff, one line.

Proved rather than asserted, because "semantically identical in bash" is exactly the kind of claim that is right until it isn't:

  • shellcheck -S error -s bash test/*.sh test/selftest/*.sh — exit 1 before, exit 0 after, same shellcheck (0.11.0).
  • harness_selftest.sh on PG18: 288 passed + 0 failed + 0 unrunnable, unchanged. The check that would go red if the sed stopped resolving the variable name is PREMISE the Makefile's recursion was actually parsed — it asserts the parser found at least one $(MAKE) -C $(X_DIR) — and it still passes, so the parse still resolves.

The gap this exposes: shellcheck runs only in .github/workflows/ci.yml. Nothing in test/ invokes it. I reported "ALL VERSIONS PASSED" for 9058515 and that was true and also could not have caught this, because the local gate has no shellcheck arm at all. That is a class of red the local gate is structurally blind to, and I will run the CI line explicitly before pushing harness changes from now on. If you want it enforced rather than remembered I will open it as its own issue instead of widening this PR.

I also grepped the class rather than just the instance: $var[ outside ${...} across test/*.sh test/selftest/*.sh has one other hit, test/bench_guards.sh:304, and it is inside a single-quoted grep pattern, so it is not an expansion and shellcheck agrees — exit 0 is over the whole corpus.

The two suites jobs were still in progress when the shellcheck job reddened; I will report their result here when the run finishes.

@linuxhikerpm linuxhikerpm left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head b4a77c923eb4ac74253c65d1c5d41ed67b5cd75e in an isolated worktree. harness_selftest.sh is 288/288 green on PG18.6, but three false-freshness paths remain. I reproduced all three rather than inferring them.

1. The source-stamp writer cannot report failure

test/lib.sh:708-710:

pgc_write_source_stamp() {
    printf '%s\n' "${2:-}" > "${1:-/dev/null}" 2>/dev/null || true
}

Both controllers wrap this function in if (...) and promise to warn when it fails (test/run_all_versions.sh:726-736, test/devloop.sh:95-105), but || true makes that branch unreachable.

Driven against an unwritable target on this head:

test/lib.sh: line 709: /proc/pgc-source-stamp: No such file or directory
write_rc=0 exists=no

So the stamp is absent, the controller reports no warning, and every child suite degrades to UNVERIFIED despite the comments explicitly saying this was fixed. Remove the status suppression and add an arm that drives the actual writer against an unwritable destination.

2. The fingerprint hashes concatenated contents without paths or boundaries

test/lib.sh:612-624 sorts filenames, then runs xargs -0 cat | md5sum. The hash therefore cannot distinguish changes that preserve the concatenated byte stream while moving bytes between translation units.

I created two source files:

/* a.c */ static int x=1;
/* b.c */ static int x=2;

Both compile separately. I then moved b.c's bytes into a.c and emptied b.c. The new source fails to compile with a redefinition, yet the fingerprint is unchanged:

before_hash=d2d6c3a12720 after_hash=d2d6c3a12720
initial_compile=0 repartitioned_compile=1
error: redefinition of ‘x’

A skip-build run consequently prints matches the binary under test for source that cannot produce that binary. Feed each relative path and an unambiguous length/delimiter into the digest, not only file contents, and mutation-test the boundary case.

3. “Keyed by major” aliases distinct installations of the same major

test/lib.sh:717-730 says one tree installs into several prefixes and each has its own binary, but pgc_source_stamp_path DIR MAJOR stores only .pgc_source_stamp.18. Two PG18 pg_configs with different pkglibdirs resolve to the same stamp:

config_a=18 config_b=18
stamp_a=/tree/.pgc_source_stamp.18
stamp_b=/tree/.pgc_source_stamp.18

This container itself has multiple PG18 installations, so the case is not hypothetical. Building current source into prefix A updates the shared stamp. A subsequent PGC_SKIP_BUILD=1 run against stale prefix B sees the current source fingerprint, while the postmaster check also passes because its newly started server is newer than B's old .so. The run then explicitly reports fresh while executing B's stale binary.

Key the record by installation identity as well as major (at minimum the normalized installed-library path or pg_config/pkglibdir) and add a two-config arm. As written, the comment states the distinction the key discards.

@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

CI finished on b4a77c9: 12/12 SUCCESS, run conclusion success, mergeStateStatus CLEAN.

success  shellcheck (test harness, errors only)
success  build (PG 15/16/17/18, x86_64 and aarch64)   8 jobs
success  build (PG 19 beta, from source)
success  suites (PG 17)
success  suites (PG 18)

Reading the breakdown rather than a bare count, because a pending check reports conclusion "", not null, and would hide inside a "0 failed".

For completeness on what the previous run did and did not say: at 9058515 the only red was the lint. suites (PG 18) was SUCCESS there and suites (PG 17) was CANCELLED by this push, not failed — so the substance had already passed on one major and has now passed on both.

Nothing outstanding from my side on this PR. It is waiting on your re-review, and on the test/lib.sh interlock with #897 for whoever merges second.

…ts were stale

Found reviewing my own PR before asking for another look.

THE ENTRY WAS UNDER A RELEASED SECTION. It sat at CHANGELOG.md:225, under
`## [1.0-alpha3] - 2026-09-02`, not under `## [Unreleased]` at line 17. So this
PR, which is not merged, claimed the freshness controller shipped in a release
tagged a week ago. A reader of the alpha3 notes would have believed it was in
the tarball they have. Moved to [Unreleased].

BOTH COUNTS IN IT WERE STALE, and both were stale by the same cause: the entry
was written before the review, and the rework that answered the review grew what
it describes without the entry moving.

  "16 arms"                     ->  27   (grep -c '^check ' on selftest 340)
  "goes from 261 checks to 277" ->  288

Measured rather than derived, because a count is a claim:

  main f2af080, a fresh worktree   accounting: 261 passed + 0 failed = 261
  this branch                       accounting: 288 passed + 0 failed = 288

I nearly published 288 - 27 = 261 as the baseline instead of measuring it. That
subtraction assumes selftest 340 is the only thing in this PR that changes the
count, and several arms in this suite SWEEP the tree rather than stating a fixed
number of checks, so a lib.sh change can move a count without adding a `check`
line. The arithmetic happened to agree with the measurement; it was not entitled
to.

The sentence now also names the three arms the review added -- the derived
build-directory set, the running-binary check driven against a live cluster, and
devloop writing the stamp -- because "27 arms" with no account of where 11 of
them came from is a number a reader cannot check.

Verified after the change:
  docs_style         9 checks   PASSED   (it reads CHANGELOG.md at two sites, so
                                          it runs whole; the em/en dash arm and
                                          the VERSION-citation arms both pass)
  harness_selftest   288 passed + 0 failed + 0 unrunnable   PASSED
  shellcheck -S error -s bash test/*.sh test/selftest/*.sh  exit 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

I reviewed this myself before asking you to look again. One defect, three parts, fixed at 6939bba. All in the changelog entry, which is the part of this PR I had not re-read since before your review.

The entry was filed under a released version

It sat at CHANGELOG.md:225, under ## [1.0-alpha3] - 2026-09-02 — not under ## [Unreleased] at line 17. So an unmerged PR claimed the freshness controller shipped in a release tagged a week ago. Someone reading the alpha3 notes would have believed it was in the tarball they already have.

Moved to [Unreleased].

Both counts in it were stale, from the same cause

The entry was written before your review; the rework that answered the review grew what the entry describes, and the entry did not move.

claimed actual
"16 arms" 27 (grep -c '^check ' test/selftest/340-…)
"goes from 261 checks to 277" 261 → 288

Measured, not derived:

main f2af0809, fresh worktree   accounting: 261 passed + 0 failed + 0 unrunnable = 261
this branch                     accounting: 288 passed + 0 failed + 0 unrunnable = 288

I nearly published 288 − 27 = 261 as the baseline instead of measuring it. That subtraction assumes selftest 340 is the only thing in this PR that moves the count, and several arms in this suite sweep the tree rather than stating a fixed number of checks — so a lib.sh change can move a count without adding a check line, and this PR changes lib.sh substantially. The arithmetic happened to agree with the measurement. It was not entitled to.

The sentence now also names where the extra 11 arms came from — the derived build-directory set, the running-binary check driven against a live cluster, and devloop writing the stamp — because "27 arms" with no account of the growth is a number a reader cannot check.

Why I am reporting a changelog fix as a finding

Because it is the same defect this PR exists to prevent, one level up. The PR's whole argument is that a stale artifact under an explicit assurance is worse than one under no assurance, since the assurance is what stops the next person checking. A changelog entry filed under a shipped release is exactly that: a specific, confident, checkable claim that is false, sitting where a reader goes instead of checking. I would have flagged it in someone else's PR.

Verified after the change

docs_style         9 checks                                PASSED
harness_selftest   288 passed + 0 failed + 0 unrunnable   PASSED
shellcheck -S error -s bash test/*.sh test/selftest/*.sh   exit 0

docs_style ran whole rather than by its CHANGELOG arm alone, per the correction you made me on the last round — it reads CHANGELOG.md at two sites, and the em/en-dash arm and the VERSION-citation arms all pass.

Still outstanding, unchanged

The test/lib.sh interlock with #897. I re-verified it against both current heads today: #898 writes pgc_write_source_stamp inside pgc_setup's build branch immediately after a successful make install, and #897 extracts that same branch into pgc_build_and_install(), which ends make installreturn 0. Whoever merges second must land the stamp write inside the extracted function, between the successful install and the return. Left in pgc_setup, the pytest harness — whose entire purpose is calling pgc_build_and_install directly — builds and installs with no stamp, and every pytest run reports freshness UNVERIFIED. Git merges it cleanly either way and no test catches it.

@jdatcmd

jdatcmd commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

For whoever merges #897 and #898 second: they touch the same block of test/lib.sh, and the
naive resolution silently disables the freshness check for the pytest harness.

I tested the merge rather than reasoning about it. One correction to how this was first described
to me, and it is good news: git does NOT merge them cleanly. It conflicts, at exactly the right
place, so a human is forced to look:

merge rc=1
CONFLICT (content): Merge conflict in test/lib.sh

The conflict is this, verbatim from the merged tree:

<<<<<<< HEAD                                    (#898)
        # ... The stamp is
        # written HERE and nowhere else. An earlier revision wrote it in the
        # skip-build branch instead, which made the check tautological ...
        pgc_write_source_stamp \
                "$(pgc_source_stamp_path "$PGC_SRCDIR" "$PGC_MAJOR")" \
                "$(pgc_source_fingerprint "$PGC_SRCDIR")"
=======                                         (#897)
        pgc_build_and_install "$PGC_SRCDIR" "$PGC_PG_CONFIG" "$PGC_MAJOR" || exit 1
>>>>>>> pr897tmp

The hazard survives the conflict, and one detail makes it worse than an ordinary tricky
resolution.
#897 extracts the build into pgc_build_and_install() (lines 150-180 in the merged
tree), which ends make install -> return 0. #898's stamp write must end up inside that
function. If the resolver keeps both sides in place — the obvious resolution — the stamp write
stays in pgc_setup while the install moves into the extracted function. Confirmed on the
conflicted tree:

pgc_build_and_install spans lines 150..180
pgc_setup             spans lines 182..458
pgc_write_source_stamp call at line 279  ->  inside pgc_setup

The pytest harness calls pgc_build_and_install directly. So it would build, install, and write no
stamp — and every pytest run would report freshness UNVERIFIED while looking perfectly fine. That
is the failure #898 exists to prevent, reintroduced by merging #898 with something else.

And the comment at the conflict site argues for the wrong resolution. "The stamp is written
HERE and nowhere else" was written to prevent a different placement bug — an earlier revision put
it in the skip-build branch and made the check tautological. That comment is now pointing the next
reader at the next placement bug. A comment that was correct when written and misleads after an
unrelated refactor is the shape worth naming, not the merge itself.

The resolution: the stamp write goes inside pgc_build_and_install, immediately after the
successful make install and before its return 0, and the comment gets reworded to say after a
successful install, wherever that happens
rather than naming a function.

This also subsumes one of my review findings on #898: devloop.sh currently gets
freshness UNVERIFIED because it installs and then runs with PGC_SKIP_BUILD=1, and only
run_all_versions.sh writes the stamp. Both are the same root cause — the stamp is written at one
call site rather than wherever the install happens.

Tested against #897 dc03d11d6417 and #898 6939bba36679.

jdatcmd added a commit that referenced this pull request Sep 9, 2026
Two changes from the #902 review, both from OffgridwithJD.

CONTEXT.md's twin rule now says to pin the SHA the twin was tested against
rather than the branch name. Their argument is the one that convinced me: a
branch name is not checkable later, and it is why they could verify my claim at
all. The harness branch moved three times while the first twin was being
written, and two of those moves changed its content -- so "blocked on #897" and
"blocked on #897 at b785795" are different claims and only one can be
falsified. Same reason a tag is read from the API rather than from a local ref,
which I got wrong earlier today and filed a false issue over.

The twin's header records that #897 moved a fourth time, to 9064a46, and
DELIBERATELY DOES NOT UPDATE THE PIN. The point of a SHA is to say what was
tested. What is recorded instead is why the pin still describes the current
head, verified here rather than taken from the push notice:

  b785795 test/pytest tree = b20ad7e
  9064a46 test/pytest tree = b20ad7e
  whole delta = 30 lines in one test/selftest/ file the harness never reads

NOT CHANGED, deliberately: the five x86_64 build failures on this PR are the
PGDG apt mirror, not this branch. The mirror is serving a Release file created
at 17:16:59 alongside a component index last modified at 09:41:12, so the index
cannot match the manifest describing it. Two attempts twenty minutes apart
produced byte-identical hashes, which rules out a race. #898 at 6939bba and #897
at b785795 both went fully green before 17:16 and both #897 at 9064a46 and this
branch fail after it, with #897's delta being thirty lines in a directory no
build job reads. aarch64 passed all five majors throughout. Patching ci.yml
around a mirror that is mid-sync would outlive the outage and get copied.

docs_style.sh: 9 checks, PASSED.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK

@jdatcmd jdatcmd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 6939bba. All three findings are closed, and I verified the load-bearing one with
the same probe that found it rather than reading the diff.

The objstore gap, closed and re-proved

My probe now refuses where it previously passed silently:

baseline                                  -- source: 1449dc9dba17 matches the binary under test
edit objstore/columnar_objstore_module.c  FATAL: the binary under test was not built from this source
                                                 source now f9951b59e2a1, binary built from 1449dc9dba17

And the fix is structural rather than a list. pgc_source_build_dirs derives the set with
find -mindepth 2 -maxdepth 2 -name Makefile, so any directory that carries its own Makefile is in
the fingerprint. objstore/ was the one that existed; the next one is covered without anybody
remembering. That is the property I asked for rather than the patch I suggested.

The unreachable arm, fixed the right way round

I said predates could not fire through any shipped path, and that selftest/340 feeding
pgc_running_binary_verdict fixture values proved its arithmetic while the call site could be
deleted with every check passing.

The answer was not to invent a reachable path or to delete the arm. pgc_check_running_binary
now takes the .so path as an argument, so the selftest points it at a file it has just touched
and the stat, the query, the verdict and the refusal all run for real — only the path is
redirected. Nothing has to disturb the installed library to prove the guard fires.

And the comment states the limit rather than hiding it: through any shipped path the postmaster is
always newer than the .so, so predates remains unreachable in normal operation. Recording that
is worth more than the arm.

devloop.sh

Now writes the stamp, sources lib.sh in a subshell so there is one fingerprint implementation
rather than two that drift, and is explicitly not || true — which was your own earlier
self-caught bug in this same PR, applied to the new site before I could raise it.

That was the finding I said I would fix first, because it is the loop a human uses while editing C.

One thing to carry into the merge

The hazard I posted on both PRs stands and matters more now that the pytest harness drives
pgc_build_and_install directly: whoever merges second must put the stamp write inside that
extracted function, not leave it in pgc_setup. Git conflicts rather than merging cleanly, so a
human is forced to look — but the comment at the conflict site argues for the wrong resolution.

Verification

12 of 12 on 6939bba, breakdown read rather than the colour, including both suites legs. That
run predates the PGDG mirror going inconsistent at 17:16, so it is a real green and not a stale one.

Approving. This is a better change than the one I reviewed, and the parts I did not ask for — the
derived build-dir set and the drivable call site — are the parts that will still be right after the
next directory is added.

@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

You are right about the comment, and I am not pushing the fix without you deciding — because a push here would inherit your approval.

The comment does argue for the wrong resolution

test/lib.sh:226-232:

# THIS RUN IS THE CONTROLLER for whatever follows in this batch: it built
# and installed, so record what the binary was built from. The stamp is
# written HERE and nowhere else. An earlier revision wrote it in the
# skip-build branch instead, ...

"written HERE and nowhere else" was meant as a statement about which branch — not the skip-build one. At the conflict it reads as a statement about which location, and the resolver's question is exactly "does this line stay in pgc_setup or move into pgc_build_and_install?" The comment answers "stay", and that is the answer that breaks it: left in the caller, the pytest harness calls pgc_build_and_install directly, builds, installs, and writes no stamp — so every pytest run reports freshness UNVERIFIED while looking healthy.

A comment that is right about the past and wrong about the future, sitting precisely where someone is forced to make a decision. Worse than no comment.

Proposed replacement

		# THIS RUN IS THE CONTROLLER for whatever follows in this batch: it built
		# and installed, so record what the binary was built from.
		#
		# THE STAMP IS WRITTEN ON THE PATH THAT BUILT AND INSTALLED, AND ON NO
		# OTHER. That is a statement about WHICH PATH, not about which line. If
		# this block is moved or extracted into a function, THE STAMP WRITE GOES
		# WITH IT -- between the successful `make install` and the return.
		# Left behind in the caller it is not written by a run that builds
		# through the extracted path; hoisted above the install it is written
		# for an install that may have failed.
		#
		# An earlier revision wrote it in the skip-build branch instead, which
		# made the check tautological -- every run recorded the source it was
		# about to compare against, so a suite measuring an edited tree reported
		# "matches the binary under test". My own red arm caught it, which is the
		# only reason this comment exists.

Same fact, stated as a property of the path rather than of the position, so it survives the extraction and argues for the correct resolution at the conflict.

Why I am asking rather than pushing

This repository does not dismiss stale reviews. Your APPROVED at 6939bba would carry to whatever I push next, and a reader of the merge sees an approved PR containing text you never read. That is small here — it is a comment in a shell file, no object-level consequence — but the decision about what your approval covers is yours, not mine.

Three ways to take it, and I am happy with any:

  1. Say the word and I push it, knowing the approval carries.
  2. Leave 6939bba as approved and merge it, and I fold the comment fix into test: a pytest harness with a vacuity-refusal layer, 74 tests (#432) #897's conflict resolution, where the resolver is looking at both sides anyway. This is the one I would pick — the comment's new wording is about the extraction, so it reads more naturally in the commit that performs it.
  3. Neither, and the note in your review plus this thread is the record. It is already written down twice on two PRs; the resolver has to open one of them.

On the merge order

Agreed that it conflicts rather than merging cleanly, and that this is the good outcome. One thing to add for whoever resolves: after either order, the check that catches a wrong resolution is not in the diff — it is running the pytest corpus and seeing whether it prints -- source: <hash> matches the binary under test or freshness UNVERIFIED (no stamp for major N). The second is the wrong resolution, it exits 0, and nothing else reports it.

Thanks for re-running the objstore probe rather than reading the diff. The predates arm being unreachable-but-recorded was the finding I would have got wrong on my own — I would have invented a path to make it fire.

@OffgridwithJD

OffgridwithJD commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Cross-posting the verified resolution for the test/lib.sh interlock you flagged in your approval: #897 (comment)

Short version: I trial-merged the two branches locally. One conflict, one hunk, and the obvious resolution ("take HEAD, it is the refactor") silently deletes the stamp write. The correct one moves it inside pgc_build_and_install, between the successful make install and return 0. Verified on the merged tree: harness_selftest 328 passed, native_projection.sh prints matches the binary under test, pytest 71 passed, and the pytest harness writes .pgc_source_stamp.18 — which is exactly what the "leave it in pgc_setup" resolution does not do.

Nothing pushed to either branch; it was a dry run. Your approval at 6939bba stands untouched, and I still have not pushed the comment rewrite we discussed.

(Edited: the first line originally read "for the interlock" — I passed this body inline to gh in double quotes and bash ran the backticked test/lib.sh as a command substitution, deleting it from the text. Also corrected the link, which pointed at the re-review comment rather than the interlock proof.)

OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Sep 9, 2026
…at was false (commandprompt#432)

THREE ITEMS, from two reviews I had not read when I asked for a re-review. I
posted a closure table against @jdatcmd's 14:45 review while his 18:05 one and
@linuxhikerpm's 16:38 one were both sitting on the PR. That is my error and it
is the reason this commit exists rather than an approval.

--- @linuxhikerpm 1: AN objstore/ EDIT WAS CERTIFIED AS ALREADY BUILT ---------

Reproduced against the branch before fixing:

    objstore_before=2799803eaeac objstore_after=2799803eaeac
    builds=1 second=already-built

source_fingerprint() read src/*.c, src/*.h, the top-level Makefile, *.control and
*.sql. objstore/ is a SEPARATELY BUILT shared library the top-level Makefile
reaches by recursion, so editing objstore/module.c left the hash unchanged and
build_once treated a stale module as current.

Its docstring said "Same input set as pgc_source_fingerprint in test/lib.sh",
which was itself false -- and this is an INDEPENDENT implementation, so rebasing
commandprompt#898 would not have fixed it. THAT is the argument for the two becoming one
implementation rather than two that happen to agree.

source_build_dirs() now derives the set by the rule the build itself follows:
src/, plus any directory carrying its own Makefile. Naming objstore/ would fix
today and fail the next time a module is added.

AND A COLLISION THE GLOB DOES NOT FIX. The hash mixed in each file's bare NAME.
With two build directories src/module.c and objstore/module.c become
interchangeable -- swap their contents and the fingerprint does not move. It now
mixes in the path relative to the tree.

--- @linuxhikerpm 2: make_cluster LEAKED ITS TREE ON A FAILED SETUP -----------

    make_cluster_error=RuntimeError
    new_roots=1 leaked=['/tmp/pgc-pytest-777-h3phhtxc']

root came from mkdtemp and then Cluster(), initdb(), start() and is_ours() ran
with no cleanup guard. conftest.py cannot clean up after it, because
`cluster, root = make_cluster(...)` never completes when the call raises. The
HANDLED is_ours() path leaked too: it stopped the cluster and left the directory.

Every exit that is not a successful return now stops whatever was started and
removes the tree. It catches BaseException rather than Exception, because a
KeyboardInterrupt during initdb leaks a datadir and a possibly-running postmaster
exactly like an error does, and the cleanup is itself guarded so a failure to
stop cannot mask the original error.

--- @jdatcmd: design/ISSUE_432_PYTEST_HARNESS.md SECTION 8a WAS FALSE ---------

He found row 7. I checked the other eight rather than fixing the one instance,
and found a second:

    9 named tests in section 8
    ABSENT  test_layer_fails_on_a_stale_library        <- his finding
    ABSENT  test_two_workers_get_different_clusters    <- found by checking the rest

Both properties ARE covered, under better names, and 8a now says which and why in
a table rather than claiming "all of section 8 is implemented and green":

  row 7 -> test_build_refusal.py. The row described an mtime-vs-postmaster check
           that is near-vacuous alone, because every suite initdb's fresh so the
           postmaster always starts after the .so. What was needed is a refusal to
           measure a binary not built from this source at all.
  row 9 -> test_the_worker_owns_its_own_cluster. Asserts port == PORT_BASE + slot
           for its OWN worker; the mapping is injective, so every worker matching
           its own id implies no two share one -- and it is checkable from inside
           one worker, which the original phrasing was not.

Section 8 is left AS WRITTEN and now says so: it is the plan from before the work,
not an index of what exists. The count is corrected (74 in 6 files) and TESTS.md
is named as the record, because TESTS.md is checked mechanically and this document
is not. Taking his advice, the doc gate is NOT extended to design/ -- a design
record describes decisions, and gating it puts a treadmill under prose that has no
reason to track the tree.

--- PROVED BY REMOVAL --------------------------------------------------------

    unmutated                          f73e0013c0a9   18 passed
    fingerprint reverts to src-only    1a1715cafb47   2 failed  (both objstore arms)
    make_cluster stops cleaning up     9cc1e703f77a   1 failed  (the leak arm)
    restored                           f73e0013c0a9   byte-exact

Each mutation asserted applied by md5 before the run, and each reddens exactly the
arms that name it.

WRITTEN TWICE, per jd's rule of 2026-09-09.
  test/pytest/test_build_refusal.py                   3 arms, behavioural. 15 -> 18.
  test/selftest/380-the-pytest-cluster-helpers.sh    14 arms, static.

The static half requires the GLOB rather than the name, and separately requires
that "objstore" does NOT appear -- the only way to tell a derivation from a list
that happens to be complete today.

Verified:
  harness_selftest   315 passed + 0 failed + 0 unrunnable   PASSED  (301 before)
  docs_style         9 checks                                PASSED
  pytest             74 passed serial, 74 passed -n 4, marker cleared for each
                     74 passed with --pgc-expect-tests 74
  shellcheck -S error -s bash test/*.sh test/selftest/*.sh   exit 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

Please hold this merge. @linuxhikerpm's review at 16:36 has three findings and all three are still live at 6939bba.

I did not read it before you approved, and I did not read it before I asked you to merge. That is twice in one session I have built on a stale view of a PR's reviews — the same mistake I just owned on #897, where their review had two real bugs in it.

I verified all three against the current head just now. They are not stale and they are not theoretical, and they are all the same failure this PR exists to prevent: reporting fresh while measuring stale.

1. The stamp writer cannot report failure

test/lib.sh:708-710:

pgc_write_source_stamp() {
	printf '%s\n' "${2:-}" > "${1:-/dev/null}" 2>/dev/null || true
}

|| true makes the writer always return 0. Both controllers wrap it in if (...) and promise to warnrun_all_versions.sh and devloop.sh — so both warning branches are unreachable. Their reproduction:

write_rc=0 exists=no

The stamp is absent, nothing warns, every child suite degrades to UNVERIFIED. And the comment I wrote in devloop.sh says NOT || true. If the stamp cannot be written ... this stops being a controller with nothing saying so — while the writer it calls swallows the failure. The comment argues for the guarantee the code does not provide.

2. The fingerprint hashes concatenated contents with no paths or boundaries

xargs -0 cat | md5sum. Moving bytes between translation units preserves the byte stream, so the hash does not move:

before_hash=d2d6c3a12720 after_hash=d2d6c3a12720
initial_compile=0 repartitioned_compile=1   error: redefinition of 'x'

Source that cannot compile reports matches the binary under test.

This is the same class I fixed on #897's Python side an hour ago, and the shell version is worse: I was mixing in each file's bare name and changed it to the path relative to the tree, because src/module.c and objstore/module.c were interchangeable. Here there is no path in the digest at all.

3. "Keyed by major" aliases distinct installations of the same major

pgc_source_stamp_path DIR MAJOR yields .pgc_source_stamp.18. This container has pg18a, pg18n and pg18_san — three PG18 installations with different pkglibdirs, all sharing one stamp. Build into one prefix, then run PGC_SKIP_BUILD=1 against another, and the source fingerprint matches while the binary is stale. The postmaster check passes too, because the freshly started server is newer than the old .so.

As they put it: the comment above that function states the distinction the key discards.

What I am doing

Fixing all three, with a reproduction first and a removal proof for each, written twice per the rule. I will not push to this branch until you say so — it is approved, and this repo does not dismiss stale reviews, so anything I push inherits your APPROVED. Given the size of these three, I think that is the wrong outcome and you should re-review; but it is your call, and the alternatives are:

  1. I push the fixes here and you re-review a branch that says APPROVED — I would not choose this.
  2. I push the fixes here and you dismiss your own approval first, so the state matches reality.
  3. You merge 6939bba as approved and I fix all three in a follow-up PR against main.

I would pick 2, because these are defects in the guarantee the PR is for, not additions. Merging a freshness controller with three false-freshness paths open is the outcome the PR exists to prevent.

Sorry for the late catch. The instrument that found it was listing every review with its timestamp instead of trusting reviewDecision, which I only started doing after being burned by it on #897 an hour ago.

@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

All three fixed and verified locally at 1aed4b8. Still not pushed — waiting on your call about the approval.

Each was reproduced on this box before being fixed, not taken on the review's word:

finding before after
writer swallows failure write_rc=0 exists=no write_rc=1
digest blind to a repartition before=bfce474cc159 after=bfce474cc159, repartitioned_compile=1 before=b37bd7e36f93 after=e808fc4e9a5e
stamp aliases installations both PG18s → .pgc_source_stamp.18 …18.d9e24bec vs …18.f8ce2624

Proved by removal

unmutated                          18e60be58200   302 passed
writer swallows failure again      7040d7d67c04     1 failed
digest reverts to concatenation    9c7e01986e67     2 failed
stamp key reverts to major only    bd42a44e1dca     3 failed
restored                           18e60be58200   byte-exact

Each mutation asserted applied by md5 before the run; each reddens exactly the arms that name it and no others. 14 new arms in selftest/340, driving the real functions. harness_selftest 288 → 302, shellcheck exit 0.

Two things worth flagging beyond the fixes

The digest defect is the same class as one I fixed on #897 today, from the other direction: there the hash mixed in each file's bare name, so src/module.c and objstore/module.c were interchangeable. Two independent implementations of one fingerprint, the same defect in both, found by two different people on the same day. That is the argument for the two becoming one implementation rather than two that happen to agree — and it is a stronger argument than the one I made when I wrote the second copy.

The stamp-key arms use fake pg_config scripts, not this box's three PG18 installations. The finding is real because pg18a, pg18n and pg18_san exist here, but an arm that depends on which majors happen to be installed tests the box rather than the function. Both controls are in there too: the same pg_config twice must give one path, and two pg_configs pointing at one prefix must share a stamp — otherwise "make them different" is satisfied by making them all different, and every run rebuilds.

What I need from you

The branch is approved, so I am still holding. My recommendation is unchanged and is option 2 from my earlier comment: dismiss your approval, I push, you re-review. These are three defects in the guarantee this PR is for, not additions to it.

If you would rather merge 6939bba as approved, say so and I will open these as a separate PR against main instead — that is option 3 and it is also fine; it just means main briefly carries a freshness controller with three known false-freshness paths.

One thing that has changed since you approved: #897 is now approved too, and I have asked you to land it first precisely because this PR has open findings and that one does not. That reverses which branch carries the stamp-write interlock — and the #898 side is the easier one, because with pgc_build_and_install already on main the stamp write has one obvious home and the misleading "written HERE and nowhere else" comment gets rewritten here rather than surviving into main. I would fold that into the same push.

The pytest twin for these 14 arms is owed under the twin rule and lands with that rebase, since test/pytest/ arrives with #897.

@jdatcmd
jdatcmd merged commit 46016fb into commandprompt:main Sep 9, 2026
12 checks passed
OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Sep 9, 2026
…at was false (commandprompt#432)

THREE ITEMS, from two reviews I had not read when I asked for a re-review. I
posted a closure table against @jdatcmd's 14:45 review while his 18:05 one and
@linuxhikerpm's 16:38 one were both sitting on the PR. That is my error and it
is the reason this commit exists rather than an approval.

--- @linuxhikerpm 1: AN objstore/ EDIT WAS CERTIFIED AS ALREADY BUILT ---------

Reproduced against the branch before fixing:

    objstore_before=2799803eaeac objstore_after=2799803eaeac
    builds=1 second=already-built

source_fingerprint() read src/*.c, src/*.h, the top-level Makefile, *.control and
*.sql. objstore/ is a SEPARATELY BUILT shared library the top-level Makefile
reaches by recursion, so editing objstore/module.c left the hash unchanged and
build_once treated a stale module as current.

Its docstring said "Same input set as pgc_source_fingerprint in test/lib.sh",
which was itself false -- and this is an INDEPENDENT implementation, so rebasing
commandprompt#898 would not have fixed it. THAT is the argument for the two becoming one
implementation rather than two that happen to agree.

source_build_dirs() now derives the set by the rule the build itself follows:
src/, plus any directory carrying its own Makefile. Naming objstore/ would fix
today and fail the next time a module is added.

AND A COLLISION THE GLOB DOES NOT FIX. The hash mixed in each file's bare NAME.
With two build directories src/module.c and objstore/module.c become
interchangeable -- swap their contents and the fingerprint does not move. It now
mixes in the path relative to the tree.

--- @linuxhikerpm 2: make_cluster LEAKED ITS TREE ON A FAILED SETUP -----------

    make_cluster_error=RuntimeError
    new_roots=1 leaked=['/tmp/pgc-pytest-777-h3phhtxc']

root came from mkdtemp and then Cluster(), initdb(), start() and is_ours() ran
with no cleanup guard. conftest.py cannot clean up after it, because
`cluster, root = make_cluster(...)` never completes when the call raises. The
HANDLED is_ours() path leaked too: it stopped the cluster and left the directory.

Every exit that is not a successful return now stops whatever was started and
removes the tree. It catches BaseException rather than Exception, because a
KeyboardInterrupt during initdb leaks a datadir and a possibly-running postmaster
exactly like an error does, and the cleanup is itself guarded so a failure to
stop cannot mask the original error.

--- @jdatcmd: design/ISSUE_432_PYTEST_HARNESS.md SECTION 8a WAS FALSE ---------

He found row 7. I checked the other eight rather than fixing the one instance,
and found a second:

    9 named tests in section 8
    ABSENT  test_layer_fails_on_a_stale_library        <- his finding
    ABSENT  test_two_workers_get_different_clusters    <- found by checking the rest

Both properties ARE covered, under better names, and 8a now says which and why in
a table rather than claiming "all of section 8 is implemented and green":

  row 7 -> test_build_refusal.py. The row described an mtime-vs-postmaster check
           that is near-vacuous alone, because every suite initdb's fresh so the
           postmaster always starts after the .so. What was needed is a refusal to
           measure a binary not built from this source at all.
  row 9 -> test_the_worker_owns_its_own_cluster. Asserts port == PORT_BASE + slot
           for its OWN worker; the mapping is injective, so every worker matching
           its own id implies no two share one -- and it is checkable from inside
           one worker, which the original phrasing was not.

Section 8 is left AS WRITTEN and now says so: it is the plan from before the work,
not an index of what exists. The count is corrected (74 in 6 files) and TESTS.md
is named as the record, because TESTS.md is checked mechanically and this document
is not. Taking his advice, the doc gate is NOT extended to design/ -- a design
record describes decisions, and gating it puts a treadmill under prose that has no
reason to track the tree.

--- PROVED BY REMOVAL --------------------------------------------------------

    unmutated                          f73e0013c0a9   18 passed
    fingerprint reverts to src-only    1a1715cafb47   2 failed  (both objstore arms)
    make_cluster stops cleaning up     9cc1e703f77a   1 failed  (the leak arm)
    restored                           f73e0013c0a9   byte-exact

Each mutation asserted applied by md5 before the run, and each reddens exactly the
arms that name it.

WRITTEN TWICE, per jd's rule of 2026-09-09.
  test/pytest/test_build_refusal.py                   3 arms, behavioural. 15 -> 18.
  test/selftest/380-the-pytest-cluster-helpers.sh    14 arms, static.

The static half requires the GLOB rather than the name, and separately requires
that "objstore" does NOT appear -- the only way to tell a derivation from a list
that happens to be complete today.

Verified:
  harness_selftest   315 passed + 0 failed + 0 unrunnable   PASSED  (301 before)
  docs_style         9 checks                                PASSED
  pytest             74 passed serial, 74 passed -n 4, marker cleared for each
                     74 passed with --pgc-expect-tests 74
  shellcheck -S error -s bash test/*.sh test/selftest/*.sh   exit 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
jdatcmd added a commit that referenced this pull request Sep 9, 2026
Two changes from the #902 review, both from OffgridwithJD.

CONTEXT.md's twin rule now says to pin the SHA the twin was tested against
rather than the branch name. Their argument is the one that convinced me: a
branch name is not checkable later, and it is why they could verify my claim at
all. The harness branch moved three times while the first twin was being
written, and two of those moves changed its content -- so "blocked on #897" and
"blocked on #897 at b785795" are different claims and only one can be
falsified. Same reason a tag is read from the API rather than from a local ref,
which I got wrong earlier today and filed a false issue over.

The twin's header records that #897 moved a fourth time, to 9064a46, and
DELIBERATELY DOES NOT UPDATE THE PIN. The point of a SHA is to say what was
tested. What is recorded instead is why the pin still describes the current
head, verified here rather than taken from the push notice:

  b785795 test/pytest tree = b20ad7e
  9064a46 test/pytest tree = b20ad7e
  whole delta = 30 lines in one test/selftest/ file the harness never reads

NOT CHANGED, deliberately: the five x86_64 build failures on this PR are the
PGDG apt mirror, not this branch. The mirror is serving a Release file created
at 17:16:59 alongside a component index last modified at 09:41:12, so the index
cannot match the manifest describing it. Two attempts twenty minutes apart
produced byte-identical hashes, which rules out a race. #898 at 6939bba and #897
at b785795 both went fully green before 17:16 and both #897 at 9064a46 and this
branch fail after it, with #897's delta being thirty lines in a directory no
build job reads. aarch64 passed all five majors throughout. Patching ci.yml
around a mirror that is mid-sync would outlive the outage and get copied.

docs_style.sh: 9 checks, PASSED.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
jdatcmd added a commit that referenced this pull request Sep 9, 2026
Two changes from the #902 review, both from OffgridwithJD.

CONTEXT.md's twin rule now says to pin the SHA the twin was tested against
rather than the branch name. Their argument is the one that convinced me: a
branch name is not checkable later, and it is why they could verify my claim at
all. The harness branch moved three times while the first twin was being
written, and two of those moves changed its content -- so "blocked on #897" and
"blocked on #897 at b785795" are different claims and only one can be
falsified. Same reason a tag is read from the API rather than from a local ref,
which I got wrong earlier today and filed a false issue over.

The twin's header records that #897 moved a fourth time, to 9064a46, and
DELIBERATELY DOES NOT UPDATE THE PIN. The point of a SHA is to say what was
tested. What is recorded instead is why the pin still describes the current
head, verified here rather than taken from the push notice:

  b785795 test/pytest tree = b20ad7e
  9064a46 test/pytest tree = b20ad7e
  whole delta = 30 lines in one test/selftest/ file the harness never reads

NOT CHANGED, deliberately: the five x86_64 build failures on this PR are the
PGDG apt mirror, not this branch. The mirror is serving a Release file created
at 17:16:59 alongside a component index last modified at 09:41:12, so the index
cannot match the manifest describing it. Two attempts twenty minutes apart
produced byte-identical hashes, which rules out a race. #898 at 6939bba and #897
at b785795 both went fully green before 17:16 and both #897 at 9064a46 and this
branch fail after it, with #897's delta being thirty lines in a directory no
build job reads. aarch64 passed all five majors throughout. Patching ci.yml
around a mirror that is mid-sync would outlive the outage and get copied.

docs_style.sh: 9 checks, PASSED.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Sep 9, 2026
…mmandprompt#907)

test/lib.sh and test/pytest/pgc_cluster.py each carried their own answer to
"what was this binary built from". On 2026-09-09 the pair produced four defects
between them -- two in each copy, and NOT ONE was found by whoever wrote that
copy:

    objstore/*.c never walked            python   @linuxhikerpm, commandprompt#897
    the bare NAME instead of the path    python   found while fixing the above
    `xargs -0 cat | md5sum`, no bounds   shell    @linuxhikerpm, commandprompt#898
    each build dir's Makefile omitted    python   found while writing the twin

The Python docstring asserted "the same input set as pgc_source_fingerprint in
test/lib.sh" throughout all four. It was false when written and stayed false
through two rounds of fixing. A prose claim of agreement is not a mechanism, and
it is worse than silence because it is what stops the next person checking.

Python, not shell, which is the opposite of what commandprompt#907 first proposed
--------------------------------------------------------------------
jd's constraint decided it: the single implementation belongs in the more
portable language. bash is largely a GNU thing; Python is present on FreeBSD and
Windows where bash is not. lib.sh already requires bash, so calling a more
portable interpreter from it cannot cost portability.

My argument against this direction was that lib.sh invokes python3 zero times, so
this escalates from "53 suites need it" to "every suite needs it at gate time".
That is true and it is not a cost, for the reason above. Measured, expecting to
report a subprocess penalty:

    shell, forking md5sum once per file    239 ms/call
    the module, one interpreter start       26 ms/call
    across 261 suites x 2 fingerprints      124 s  ->  13 s

The portable direction is also 9x faster. I had it backwards in both dimensions.

A fifth defect, which unifying them found
------------------------------------------
`sort -z` orders by LOCALE COLLATION, and nothing in this harness pins a locale.
The same tree fingerprinted two ways depending on whose machine it was:

    LC_ALL=C             6d122a7158d5
    LC_ALL=en_US.UTF-8   0b59bd75fa4f

en_US.UTF-8 is a common desktop default, so this is a developer stamping a tree
and CI reading it back and calling the binary stale -- a false FATAL arriving
from the environment rather than from the source. The module sorts BYTES, which
is what LC_ALL=C produced and what every stamp already on disk was written with,
so no existing stamp is invalidated. Arms in both harnesses.

Equivalence, established rather than asserted
----------------------------------------------
A differential run of the module against the shell it replaces, over trees built
to break the ways this pair has actually broken. 17 shapes, manifest AND
fingerprint compared:

    the real source tree, minimal, a recursed module, a dir with sources but no
    Makefile, collation-sensitive names, a symlinked source file, a symlinked
    build directory, no src/, an empty tree, root .control and .sql, non-source
    files, spaces and punctuation, unicode, a Makefile at depth 3, a trailing
    slash, a /./ segment, five recursed modules

    AGREE=17  DIVERGE=0

Two of those are subtle enough to be worth naming. `find -type f` tests the LINK,
so a symlinked source is not in the shell's manifest, while `pathlib.is_file()`
FOLLOWS it and would have added one; the module excludes symlinks explicitly.
And `find` does not descend a symlinked directory, so build dirs discovered
through one differ -- which is why the module canonicalises the root first.

The mechanism of two arms had to change with the implementation
----------------------------------------------------------------
The failed-digest arms in 340 and test_build_refusal.py stubbed `md5sum` on PATH.
The digest is hashlib now, which no PATH can reach, so the stub would have left
both arms GREEN while testing nothing -- the exact shape this corpus refuses.

A real read failure needs a real reader who is denied, and root is denied
nothing: chmod 000 is invisible to it. Measured before the arms were rewritten:

    as root      28a7149e07ae   <- reads the mode-000 file regardless
    as postgres  (empty)        <- the failure the arm needs

So the tree is built outside any mode-0700 directory and read by a second user,
with a premise asserting that reader agrees with a privileged one WHILE nothing
is denied -- otherwise the arm measures the user switch rather than the failure.
Where no non-root user exists it records expect.cannot_run rather than passing.

And the arm that would catch this issue recurring
--------------------------------------------------
selftest 380's static guards follow the fingerprint to its new file, plus three
new arms: neither caller may keep a private implementation, and the module may
import nothing from test/pytest/. A static assertion of ABSENCE is the shape that
most often cannot fail, so each was proved against the REAL files rather than
only against fixtures -- a fixture proves the pattern matches something, not that
the arm aimed at the real file would fire:

    pgc_cluster.py grows a private digest      HELD
    lib.sh grows a private md5sum loop         HELD
    the module imports from the pytest tree    HELD

    harness_selftest   407 passed + 0 failed + 0 unrunnable, rc=0
    pytest corpus       91 passed
    docs_style           9 checks PASSED
    shellcheck -S error  clean

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Sep 9, 2026
…mmandprompt#907)

test/lib.sh and test/pytest/pgc_cluster.py each carried their own answer to
"what was this binary built from". On 2026-09-09 the pair produced four defects
between them -- two in each copy, and NOT ONE was found by whoever wrote that
copy:

    objstore/*.c never walked            python   @linuxhikerpm, commandprompt#897
    the bare NAME instead of the path    python   found while fixing the above
    `xargs -0 cat | md5sum`, no bounds   shell    @linuxhikerpm, commandprompt#898
    each build dir's Makefile omitted    python   found while writing the twin

The Python docstring asserted "the same input set as pgc_source_fingerprint in
test/lib.sh" throughout all four. It was false when written and stayed false
through two rounds of fixing. A prose claim of agreement is not a mechanism, and
it is worse than silence because it is what stops the next person checking.

Python, not shell, which is the opposite of what commandprompt#907 first proposed
--------------------------------------------------------------------
jd's constraint decided it: the single implementation belongs in the more
portable language. bash is largely a GNU thing; Python is present on FreeBSD and
Windows where bash is not. lib.sh already requires bash, so calling a more
portable interpreter from it cannot cost portability.

My argument against this direction was that lib.sh invokes python3 zero times, so
this escalates from "53 suites need it" to "every suite needs it at gate time".
That is true and it is not a cost, for the reason above. Measured, expecting to
report a subprocess penalty:

    shell, forking md5sum once per file    239 ms/call
    the module, one interpreter start       26 ms/call
    across 261 suites x 2 fingerprints      124 s  ->  13 s

The portable direction is also 9x faster. I had it backwards in both dimensions.

A fifth defect, which unifying them found
------------------------------------------
`sort -z` orders by LOCALE COLLATION, and nothing in this harness pins a locale.
The same tree fingerprinted two ways depending on whose machine it was:

    LC_ALL=C             6d122a7158d5
    LC_ALL=en_US.UTF-8   0b59bd75fa4f

en_US.UTF-8 is a common desktop default, so this is a developer stamping a tree
and CI reading it back and calling the binary stale -- a false FATAL arriving
from the environment rather than from the source. The module sorts BYTES, which
is what LC_ALL=C produced and what every stamp already on disk was written with,
so no existing stamp is invalidated. Arms in both harnesses.

Equivalence, established rather than asserted
----------------------------------------------
A differential run of the module against the shell it replaces, over trees built
to break the ways this pair has actually broken. 17 shapes, manifest AND
fingerprint compared:

    the real source tree, minimal, a recursed module, a dir with sources but no
    Makefile, collation-sensitive names, a symlinked source file, a symlinked
    build directory, no src/, an empty tree, root .control and .sql, non-source
    files, spaces and punctuation, unicode, a Makefile at depth 3, a trailing
    slash, a /./ segment, five recursed modules

    AGREE=17  DIVERGE=0

Two of those are subtle enough to be worth naming. `find -type f` tests the LINK,
so a symlinked source is not in the shell's manifest, while `pathlib.is_file()`
FOLLOWS it and would have added one; the module excludes symlinks explicitly.
And `find` does not descend a symlinked directory, so build dirs discovered
through one differ -- which is why the module canonicalises the root first.

The mechanism of two arms had to change with the implementation
----------------------------------------------------------------
The failed-digest arms in 340 and test_build_refusal.py stubbed `md5sum` on PATH.
The digest is hashlib now, which no PATH can reach, so the stub would have left
both arms GREEN while testing nothing -- the exact shape this corpus refuses.

A real read failure needs a real reader who is denied, and root is denied
nothing: chmod 000 is invisible to it. Measured before the arms were rewritten:

    as root      28a7149e07ae   <- reads the mode-000 file regardless
    as postgres  (empty)        <- the failure the arm needs

So the tree is built outside any mode-0700 directory and read by a second user,
with a premise asserting that reader agrees with a privileged one WHILE nothing
is denied -- otherwise the arm measures the user switch rather than the failure.
Where no non-root user exists it records expect.cannot_run rather than passing.

And the arm that would catch this issue recurring
--------------------------------------------------
selftest 380's static guards follow the fingerprint to its new file, plus three
new arms: neither caller may keep a private implementation, and the module may
import nothing from test/pytest/. A static assertion of ABSENCE is the shape that
most often cannot fail, so each was proved against the REAL files rather than
only against fixtures -- a fixture proves the pattern matches something, not that
the arm aimed at the real file would fire:

    pgc_cluster.py grows a private digest      HELD
    lib.sh grows a private md5sum loop         HELD
    the module imports from the pytest tree    HELD

    harness_selftest   407 passed + 0 failed + 0 unrunnable, rc=0
    pytest corpus       91 passed
    docs_style           9 checks PASSED
    shellcheck -S error  clean

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants